Skip to content

docs: add the module and versioning design - #147

Merged
thiremani merged 8 commits into
masterfrom
docs-module-design
Oct 24, 2026
Merged

thiremani merged 8 commits into
masterfrom
docs-module-design

Conversation

@thiremani

@thiremani thiremani commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Adds docs/Pluto Module Design.md, a design record for modules, dependencies and versioning. Nothing in it is implemented yet: today a directory is the unit of compilation, pt.mod names the module path, and a module cannot use another.

What it records

  • No visibility, as decided earlier: everything a module defines is part of its contract, since functions share no mutable state.
  • Dependencies in pt.mod, with no import statements: each dependency with the minimum release it needs and an optional alias, usable from every .pt and .spt file. pt.mod also names the oldest compiler the module builds with.
  • The standard library ships with the compiler, as a set of libraries. int, float, str, array, complex and math are aliased as . by default and can be re-aliased in pt.mod, so code writes Int(23.2), and there is no implicit std namespace. A function that serves several types lives in one library, such as math's Abs, for the argument types it supports: Min and Max take ordered numbers only.
  • No shadowing: functions (by name and input count), constants and struct types each keep a namespace, as in the compiler today, and a name defined twice in one, across a module's own definitions, the standard libraries aliased as . and dependencies aliased as ., is an error. Prefixes are a namespace too. A module sees standard libraries, functions, constants and struct types as of its minimum compiler, so a later library takes no default alias either, and only an unpublished program root may alias a dependency as ..
  • Versions in symbols: a dependency's symbols carry its module path and the selected release, which the C ABI already mangles. Majors coexist in one program and their struct types stay distinct, though a generic template accepts either. Struct types in symbols carry only their name today (Struct.Mangle), so that distinction is still to be built.
  • Minimal version selection, as in Go, with no lock file. Published releases never change, so the same root pt.mod selects the same releases; the binary also depends on the source, compiler, platform and build settings. Compiled code is cached by everything it was compiled from, every release it depends on transitively included.
  • pt.sum and release records: pt.sum verifies dependency manifests and sources and never selects a version, and a mismatch fails the build. A release's record is its checksum-log entry with the proxy's copy; a Git tag alone cannot vouch for contents. A private module's first checksum is checked against a log its owner runs, or taken from the first authenticated download.
  • Go-style hosting: releases are tags in each module's own repository, with an optional proxy that keeps copies and an optional public checksum log. Before recording a release, the log reruns the API comparison against the highest release below it in its major line, from its own copy, and refuses too low a version number; it runs no tests and checks no coverage. Private modules skip both services.
  • C callers: wrapper symbols keep the major and drop the minor and patch, and structs reach C only as opaque handles, so a struct can still gain a field in a Minor release.
  • Programs without a module compile standalone with the compiler's standard library; any other module needs a pt.mod.
  • SemVer versions picked when publishing, from a table of minimum categories: publishing numbers each release itself, and the publisher may only ask for a higher category. A function is identified by its name and input count; constants promise their exact values; a struct may gain a field in a Minor release, since literals name their fields and omitted ones take their zero value, and a struct constant keeps its value while its existing fields keep their types and values. Reordering fields is Major.
  • The contract is what declarations state, not the argument types tests happen to use, so tests stay representative examples. Rerun tests catch what their assertions or type-sensitive operations expose, and the publisher declares the rest; a narrowing that escapes is a breaking change the release should have declared.
  • Publishing steps: build the API manifest, compare it with the release being updated, work out the category from the detected changes and the publisher's declared additions and breaks, run the module's tests, which must pass, and the previous release's tests, whose failures must each be explained by a detected change, a declared break or bug fix, or something outside the contract, require every function body to run at least once, number the release, refusing a lower category than the changes require, and tag it, having the checksum log record a public module's release.
  • No deprecation marks: a Major release may remove or change anything, majors coexist so each dependent moves when ready, and release notes list the detected removals and changes plus the publisher's declared breaks and migration steps.
  • Development stays permissive: an optional reachability report, execution coverage from tests, and CI decides how strict to be.
  • Open questions: how pt.mod spells dependencies, aliases and the minimum compiler, and how code names a dependency's function; who runs the proxy and the checksum log, and how a private module opts out; whether raising the minimum compiler is Minor or Major; what a conversion does with text it cannot read or a value it cannot hold; what module path a standalone program uses; how a publisher declares an addition, break or bug fix and asks for a higher category; backports; whether the major goes in the path or only in the version; what 0.y.z releases promise; which functions get a stable C wrapper; and an inferred check of what each input must support.

Commits

  1. docs: the design record.
  2. docs: from review: a failing test from the previous release must be explained rather than always stopping publishing; struct constants and Minor field additions; field order; generic calls across majors; what rerun tests catch; the visibility rationale.
  3. docs: drops deprecation marks, so compiling never warns about future removals.
  4. docs: from review: release notes include declared breaks and migration steps, and the manifest keeps struct field order.
  5. docs: publishing picks the version number, so a publisher never chooses one and can only ask for a higher category.
  6. docs: from review: publishers declare compatible additions the comparison cannot see (Minor) as well as breaks (Major); release notes are generated; running every function shows it runs, not that it is needed; backports join the open questions.
  7. docs: settles the open questions on dependencies and releases: pt.mod without imports, the standard library and its . aliases, no shadowing, minimal version selection with a transitive cache key, versions in symbols, pt.sum, release records and hosting, C callers and standalone programs.
  8. docs: from review: collision rules for each kind of name and for prefixes; minimum-compiler visibility for libraries, constants and struct types; no implicit std namespace; Min and Max for ordered numbers; private first use; what the checksum log checks and against which release; conversions join the open questions.

Verification

  • Docs only: no commit on this branch changes code. gofmt, go vet and go test -race ./lexer ./parser ./compiler pass, and python3 test.py --leak-check passes 84/84 with every leak check clean, at d7f23f5; the seventh and eighth commits change only the doc.
  • The facts the doc relies on were checked: struct literals may omit fields (tests/struct prints Ada 28 0), a template is keyed by name and input count, versioned module paths are valid and mangled (C ABI spec §1.1 and §4.3), and [Double(0:0)] never runs Double's body.
  • For the seventh commit: two definitions of one name and input count are already an error (Function Twice with 1 parameters has been previously defined at /a.pt:1:7); Struct.Mangle (compiler/types.go:485) mangles a struct by its name alone; return types are not mangled (C ABI spec §3.4); native and portable PLUTO_TARGET_CPU builds are cached apart; and with no pt.mod above it, the compiler stops with pt.mod not found ... upward.
  • For the eighth commit: a constant Pi and a function Pi(x) compile together in one module, as do a constant or a function named after a struct type, so functions, constants and struct types are separate namespaces today.
  • Merges cleanly with master after feat(compiler)!: require definite function outputs and stop functions returning ranges #145: this branch only adds a new file, and git merge-tree of the two is clean.

🤖 Generated with Claude Code

thiremani and others added 8 commits October 10, 2026 13:58
Records the decisions taken so far for modules, imports and versioning,
none of them implemented yet: no visibility, so everything a module
defines is part of its contract; the major version in the module path,
so majors coexist; SemVer categories enforced when publishing, from what
declarations state rather than from tested argument types; the publishing
steps, including rerunning the previous release's tests and requiring
every function body to run in the module's tests; deprecation; and
development reports that keep compilation permissive. Lists the
questions still open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A failing test from the previous release stops publishing only when
  nothing explains it: a change the comparison found, a declared break or
  bug fix, or something outside the contract such as a struct's printed
  form. Before, any failure stopped publishing, which ruled out every
  Major release that removes a function.
- A struct constant keeps its value while every field it had keeps its
  type and value, so a Minor field addition no longer counts as changing
  the constants of that struct type. Reordering fields is Major.
- Distinct struct types across majors don't stop a template from
  accepting either one.
- Rerun tests catch only what their assertions or type-sensitive
  operations expose, and a narrowing that escapes is a breaking change the
  release should have declared.
- The visibility rationale is that functions share no mutable state;
  functions can print and read constants, so the stronger claims are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Minor release no longer marks things as going away, so compiling never
warns about what a later Major release will remove. A Major release may
remove or change anything; each dependent moves to it when it chooses,
since majors coexist, and the release notes list every removal and
change, taken from the comparison with the release before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… order

The comparison only sees declarations, so a behavior change such as
`Double(x)` going from `x * 2` to `x * 3` never shows up in it. Release
notes now list the removals and changes the comparison detects together
with the breaking changes the publisher declares and their migration
steps, and a module's own documentation can give advance notice of
removals.

The API manifest records each struct's ordered field names and types,
since reordering fields is Major and the checker has to see it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The publisher no longer proposes a version for the checker to accept or
reject. Publishing works out the category from the detected changes and
any declared break, and numbers the release itself; the publisher may
ask for a higher category, never a lower one. Publishing reports the
version it picked and why, and a request for a lower category is refused
with the change that rules it out.

Adds an open question: how a publisher declares a break or a bug fix
and asks for a higher category.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Publishing worked out a release's category from detected changes and
declared breaks only, so a template that newly accepts more argument
types, which the comparison cannot see, would have been numbered as a
Patch although the table requires a Minor. The publisher now declares
compatible additions the comparison cannot see as well as breaks, and
the category is the highest any detected or declared change requires.

Also states that release notes are generated, so publishers never write
out changed functions, constants or fields; that requiring every
function body to run shows each function runs, not that it is needed;
and adds backports to the open questions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records the decisions taken in review for how modules use each other,
which the design had left as open questions:

- There are no import statements. A module lists its dependencies in
  pt.mod, each with the minimum release it needs and an optional alias,
  and names the oldest compiler it builds with.
- The standard library ships with the compiler as a set of libraries:
  int, float, str, array, complex and math are aliased as . by default
  and can be re-aliased in pt.mod, and there is no std prefix.
- A name and input count may be defined once among a module's own
  functions, the standard libraries aliased as . and dependencies
  aliased as .; a second definition is an error, never a shadow. A
  module sees standard names as of its minimum compiler, and only an
  unpublished program root may alias a dependency as ., since a Minor
  release of it could otherwise break builds that cannot fix the clash.
- Releases are chosen by minimal version selection, as in Go, with no
  lock file. Published releases never change, so the same root pt.mod
  selects the same releases, while the binary also depends on the
  source, compiler and build settings. Compiled code is cached by
  everything it was compiled from, every release it depends on
  transitively included, since a struct reached through an unchanged
  dependency still changes what a module compiles to.
- Symbols carry the selected release. Struct types in symbols still
  carry only their name (Struct.Mangle), so telling a v1 struct from a
  v2 one is future work.
- pt.sum, committed beside pt.mod, verifies dependency manifests and
  sources without selecting versions. Releases are tags in each
  module's own repository, as in Go, with an optional proxy that keeps
  copies and an optional checksum log that rechecks each release's
  version number against its API changes before recording it.
- C callers reach a module through wrappers whose symbols keep the
  major, and which hand out structs only as opaque handles, so a struct
  can still gain a field in a Minor release.
- A directory without a pt.mod compiles as a standalone program with
  the compiler's standard library; today the compiler stops without one.

The open questions now cover how pt.mod spells these, who runs the
proxy and the log, whether raising the minimum compiler is Minor or
Major, what 0.y.z releases promise and what module path a standalone
program uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fills gaps a review found in the module design:

- Names without a prefix are checked in the compiler's three
  namespaces: functions by name and number of inputs, constants by
  name, a struct's constants included, and struct types by name. A
  constant Pi and a function Pi(x) may coexist, as they already can
  within a module, and a module's constant of a struct type defined
  elsewhere uses that type. Prefixes form a namespace of their own.
- A module's minimum compiler hides later standard libraries,
  functions at each number of inputs, constants and struct types, so a
  new standard library cannot take a default alias a module already
  uses.
- There is no implicit std namespace: std.Int is an error unless a
  module gives a library the alias std.
- Min and Max take ordered numbers only.
- A private module's first checksum is checked against a log its owner
  runs, or taken from the first authenticated download, as Go does for
  modules outside its checksum database; a later mismatch still fails.
- The checksum log compares a release with the highest release below
  it in its major line, from its own copy, and checks only the version
  number against the detected API changes, not tests, coverage or what
  a publisher would declare.
- What a conversion does with unreadable text or a value it cannot
  hold joins the open questions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thiremani
thiremani merged commit 12691fa into master Oct 24, 2026
2 checks passed
@thiremani
thiremani deleted the docs-module-design branch October 24, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant